Active Record
=============

Хотя DAO Yii справляется практически с любыми задачами, касающимися работы с БД, почти наверняка
90% времени уйдёт на написание SQL-запросов, реализующих общие операции CRUD (создание, чтение, обновление и удаление).
Кроме того, код, перемешанный с SQL-выражениями, поддерживать проблематично.
Для решения этих проблем мы можем воспользоваться Active Record.

Active Record реализует популярный подход объектно-реляционного проецирования (ORM).
Каждый класс AR отражает таблицу (или представление) базы данных, экземпляр AR — строку в этой таблице,
а общие операции CRUD реализованы как методы AR. В результате мы можем использовать более
объектно-ориентированный подход доступа к данным. Например, используя следующий код, можно вставить новую строку в
таблицу `tbl_post`:

~~~
[php]
$post=new Post;
$post->title='тестовая запись';
$post->content='содержимое записи';
$post->save();
~~~

Ниже мы покажем, как настроить и использовать AR для реализации CRUD-операций,
а в следующем разделе — как использовать AR для работы со связанными таблицами.
Для примеров в этом разделе мы будем использовать следующую таблицу. Обратите внимание,
что при использовании БД MySQL в SQL-выражении ниже `AUTOINCREMENT` следует заменить
на `AUTO_INCREMENT`.

~~~
[sql]
CREATE TABLE tbl_post (
	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
	title VARCHAR(128) NOT NULL,
	content TEXT NOT NULL,
	create_time INTEGER NOT NULL
);
~~~

> Note|Примечание: AR не предоставляет решения всех задач, касающихся работы с базами данных.
Лучше всего использовать AR для моделирования таблиц в конструкциях PHP и для
несложных SQL-запросов. В сложных случаях следует использовать Yii DAO.


Соединение с базой данных
--------------------------

Для работы AR требуется подключение к базе данных. По умолчанию предполагается, что
компонент приложения `db` предоставляет необходимый экземпляр класса [CDbConnection],
который отвечает за подключение к базе. Ниже приведён пример конфигурации приложения:

~~~
[php]
return array(
	'components'=>array(
		'db'=>array(
			'class'=>'system.db.CDbConnection',
			'connectionString'=>'sqlite:path/to/dbfile',
			// включить кэширование схем для улучшения производительности
			// 'schemaCachingDuration'=>3600,
		),
	),
);
~~~

> Tip|Подсказка: Поскольку для получения информации о полях таблицы AR использует метаданные,
требуется некоторое время для их чтения и анализа. Если не предполагается, что схема базы данных будет меняться, то
следует включить кэширование схемы, установив для атрибута [CDbConnection::schemaCachingDuration]
любое значение больше нуля.

В настоящий момент AR поддерживает следующие СУБД:

   - [MySQL 4.1 и выше](http://www.mysql.com)
   - [MariaDB](https://mariadb.com)
   - [PostgreSQL 7.3 и выше](http://www.postgres.com)
   - [SQLite 2 и 3](http://www.sqlite.org)
   - [Microsoft SQL Server 2000 и выше](http://www.microsoft.com/sqlserver/)
   - [Oracle](http://www.oracle.com)

Если вы хотите использовать другой компонент вместо `db` или предполагаете,
используя AR, работать с несколькими БД, то следует переопределить метод
[CActiveRecord::getDbConnection()]. Класс [CActiveRecord] является базовым классом
для всех классов AR.

> Tip|Подсказка: Есть несколько способов для работы AR с несколькими БД.  Если
схемы используемых баз различаются, то можно создать разные базовые классы AR с
различной реализацией метода [getDbConnection()|CActiveRecord::getDbConnection].
В противном случае проще будет динамически менять статическую переменную [CActiveRecord::db].

Определение AR-класса
---------------------

Для доступа к таблице БД, прежде всего, требуется определить класс AR путём наследования
класса [CActiveRecord]. Каждый класс AR представляет одну таблицу базы данных, а экземпляр класса —
строку в этой таблице. Ниже приведён минимальный код, требуемый для определения класса AR,
представляющего таблицу `tbl_post`.

~~~
[php]
class Post extends CActiveRecord
{
	public static function model($className=__CLASS__)
	{
		return parent::model($className);
	}

	public function tableName()
	{
		return 'tbl_post';
	}
}
~~~

> Tip|Подсказка: Поскольку классы AR часто появляются во многих местах кода,
> мы можем вместо включения классов по одному, подключить всю директорию с AR-классами.
> Например, если AR-классы находятся в директории `protected/models`,
> мы можем сконфигурировать приложение следующим образом:
> ~~~
> [php]
> return array(
> 	'import'=>array(
> 		'application.models.*',
> 	),
> );
> ~~~

По умолчанию имя AR-класса совпадает с названием таблицы в базе данных.
Если они различаются, потребуется переопределить метод [tableName()|CActiveRecord::tableName].
Метод [model()|CActiveRecord::model] объявляется для каждого AR-класса.

> Info|Информация: В случае использования [префиксов таблиц](/doc/guide/database.dao#using-table-prefix)
> метод [tableName()|CActiveRecord::tableName] AR-класса
> может быть переопределён следующим образом:
> ~~~
> [php]
> public function tableName()
> {
>     return '{{post}}';
> }
> ~~~
> Вместо того чтобы возвращать полное имя таблицы, мы возвращаем
> имя таблицы без префикса и заключаем его в двойные фигурные скобки.

Значения полей в строке таблицы доступны как атрибуты соответствующего экземпляра AR-класса.
Например, код ниже устанавливает значение для атрибута `title`:

~~~
[php]
$post=new Post;
$post->title='тестовая запись';
~~~

Хотя мы никогда не объявляем заранее свойство `title` класса `Post`,
мы, тем не менее, можем обратиться к нему как в коде выше. Это возможно потому, что
`title` является полем таблицы `tbl_post`, и [CActiveRecord] делает его доступным в качестве свойства
благодаря магическому методу PHP `__get()`. Если аналогичным образом обратиться к несуществующему полю,
будет выброшено исключение.

> Info|Информация: В данном руководстве мы именуем столбцы и таблицы в нижнем регистре, так
как различные СУБД работают с регистрозависимыми именами по-разному.
Например, PostgreSQL считает имена столбцов регистронезависимыми по умолчанию,
и мы должны заключать их в кавычки в условиях запроса, если имена содержат заглавные буквы.
Использование нижнего регистра помогает избежать подобных проблем.

AR опирается на правильно определённые первичные ключи таблиц БД. Если в таблице
нет первичного ключа, то требуется указать в соответствующем классе AR столбцы,
которые будут использоваться как первичный ключ. Сделать это можно путём
перекрытия метода `primaryKey()`:

~~~
[php]
public function primaryKey()
{
	return 'id';
	// Для составного первичного ключа следует использовать массив:
	// return array('pk1', 'pk2');
}
~~~

Создание записи
---------------

Для добавления новой строки в таблицу БД нам необходимо создать новый экземпляр
соответствующего класса, присвоить значения атрибутам, ассоциированным с полями таблицы,
и вызвать метод [save()|CActiveRecord::save] для завершения добавления.

~~~
[php]
$post=new Post;
$post->title='тестовая запись';
$post->content='содержимое тестовой записи';
$post->create_time=time();
$post->save();
~~~

Если первичный ключ таблицы автоинкрементный, то после добавления экземпляр AR
будет содержать обновленное значение первичного ключа. В примере выше свойство
`id` всегда будет содержать значение первичного ключа новой записи.

Если поле задано в схеме таблицы с некоторым статическим значением по умолчанию
(например, строка или число), то после создания экземпляра соответствующее свойство
экземпляра AR будет автоматически содержать это значение. Один из способов
поменять это значение — прописать его в AR-классе.

~~~
[php]
class Post extends CActiveRecord
{
	public $title='пожалуйста, введите заголовок';
	…
}

$post=new Post;
echo $post->title;  // отобразится: пожалуйста, введите заголовок
~~~

До сохранения записи (добавления или обновления) атрибуту
может быть присвоено значение типа [CDbExpression]. Например, для сохранения текущей даты,
возвращаемой функцией MySQL `NOW()`, можно использовать следующий код:

~~~
[php]
$post=new Post;
$post->create_time=new CDbExpression('NOW()');
// $post->create_time='NOW()'; этот вариант работать не будет
// т.к. значение 'NOW()' будет воспринято как строка
$post->save();
~~~

> Tip|Подсказка: Несмотря на то что AR позволяет производить различные операции без
написания громоздкого SQL, часто необходимо знать, какой SQL выполняется на самом деле.
Для этого необходимо включить [журналирование](/doc/guide/topics.logging).
Например, чтобы выводить выполненные SQL-запросы в конце каждой страницы, мы можем включить
[CWebLogRoute] в настройках приложения.
Можно установить значение параметра [CDbConnection::enableParamLogging] в true для отображения
значений параметров запросов.

Чтение записи
--------------

Для чтения данных из таблицы базы данных можно использовать методы `find`:

~~~
[php]
// найти первую строку, удовлетворяющую условию
$post=Post::model()->find($condition,$params);
// найти строку с указанным значением первичного ключа
$post=Post::model()->findByPk($postID,$condition,$params);
// найти строку с указанными значениями атрибутов
$post=Post::model()->findByAttributes($attributes,$condition,$params);
// найти первую строку, используя некоторое выражение SQL
$post=Post::model()->findBySql($sql,$params);
~~~

*Выше мы вызываем метод `find` через `Post::model()`. Запомните, что статический метод `model()`
обязателен для каждого AR-класса. Этот метод возвращает экземпляр AR, используемый для
доступа к методам уровня класса (что-то схожее со статическими методами класса) в контексте объекта.*

Если метод `find` находит строку, соответствующую условиям запроса, он возвращает экземпляр
класса `Post`, свойства которого содержат значения соответствующих полей строки таблицы. Далее мы
можем использовать загруженные значения аналогично обычным свойствам объектов, например, `echo $post->title;`.

В случае если в базе нет данных, соответствующих условиям запроса, метод `find` вернет значение null.

Параметры `$condition` и `$params` используются для уточнения запроса. В данном случае `$condition`
может быть строкой, соответствующей оператору `WHERE` в SQL-выражении, а `$params` — массивом параметров, значения
которых должны быть привязаны к маркерам, указанным в `$condition`. Например:

~~~
[php]
// найдём строку, для которой postID равен 10
$post=Post::model()->find('postID=:postID', array(':postID'=>10));
~~~

> Note|Примечание: В примере выше нам может понадобиться заключить в кавычки
> обращение к столбцу `postID` для некоторых СУБД. Например, если мы используем
> СУБД PostgreSQL, нам следует писать условие как `"postID"=:postID`, потому
> что PostgreSQL по умолчанию считает имя столбца регистронезависимым.

Кроме того, можно использовать `$condition` для указания более сложных условий запроса. Вместо строки параметр
`$condition` может быть экземпляром класса [CDbCriteria], который позволяет указать иные условия
помимо выражения `WHERE`. Например:

~~~
[php]
$criteria=new CDbCriteria;
$criteria->select='title';  // выбираем только поле 'title'
$criteria->condition='postID=:postID';
$criteria->params=array(':postID'=>10);
$post=Post::model()->find($criteria); // $params не требуется
~~~

Обратите внимание, если в качестве условия запроса используется [CDbCriteria], то параметр
`$params` уже не нужен, поскольку его можно указать непосредственно в [CDbCriteria], как показано выше.

Помимо использования [CDbCriteria], есть другой способ указать условие — передать методу
массив ключей и значений, соответствующих именам и значениям свойств критерия. Пример выше можно переписать
следующим образом:

~~~
[php]
$post=Post::model()->find(array(
	'select'=>'title',
	'condition'=>'postID=:postID',
	'params'=>array(':postID'=>10),
));
~~~

> Info|Информация: В случае когда условие заключается в соответствии значениям некоторых полей, можно воспользоваться
методом [findByAttributes()|CActiveRecord::findByAttributes], где параметр `$attributes` представляет собой
массив значений, проиндексированных по имени поля. В некоторых фреймворках эта задача решается путём использования методов
типа `findByNameAndTitle`. Хотя такой способ и выглядит привлекательно, часто он вызывает путаницу и проблемы, связанные с чувствительностью
имён полей к регистру.

В случае если условию запроса отвечает множество строк, мы можем получить их все, используя
методы `findAll`, приведённые ниже. Как мы отметили ранее, каждый из этих методов имеет `find` аналог.

~~~
[php]
// найдём все строки, удовлетворяющие условию
$posts=Post::model()->findAll($condition,$params);
// найдём все строки с указанными значениями первичного ключа
$posts=Post::model()->findAllByPk($postIDs,$condition,$params);
// найдём все строки с указанными значениями атрибутов
$posts=Post::model()->findAllByAttributes($attributes,$condition,$params);
// найдём все строки, используя SQL-выражение
$posts=Post::model()->findAllBySql($sql,$params);
~~~

В отличие от `find`, методы `findAll`, в случае если нет ни одной строки, удовлетворяющей запросу,
возвращают не null, а пустой массив.

Помимо методов `find` и `findAll`, описанных выше, для удобства также доступны следующие методы:

~~~
[php]
// определим количество строк, удовлетворяющих условию
$n=Post::model()->count($condition,$params);
// определим количество строк, используя указанное SQL-выражение
$n=Post::model()->countBySql($sql,$params);
// определим, есть ли хотя бы одна строка, удовлетворяющая условию
$exists=Post::model()->exists($condition,$params);
~~~

Обновление записи
---------------

Заполнив экземпляр AR значениями полей, мы можем изменить их и сохранить обратно в БД.

~~~
[php]
$post=Post::model()->findByPk(10);
$post->title='new post title';
$post->save(); // сохраняем изменения
~~~

Как можно было заметить, мы используем метод [save()|CActiveRecord::save] как для добавления, так и для обновления записей.
Если экземпляр AR создан с использованием оператора `new`, то вызов метода [save()|CActiveRecord::save]
приведёт к добавлению новой строки в базу данных. Если же экземпляр AR создан как результат вызова методов
`find` и `findAll`, вызов метода [save()|CActiveRecord::save] обновит данные существующей строки в таблице.
На самом деле, можно использовать свойство [CActiveRecord::isNewRecord] для указания, является экземпляр AR новым или нет.

Кроме того, можно обновить одну или несколько строк в таблице без их предварительной загрузки. Для этого в
AR существуют следующие методы уровня класса:

~~~
[php]
// обновим строки, отвечающие заданному условию
Post::model()->updateAll($attributes,$condition,$params);
// обновим строки, удовлетворяющие заданному условию и значению первичного ключа (или нескольким значениям ключей)
Post::model()->updateByPk($pk,$attributes,$condition,$params);
// обновим поля-счётчики в строках, удовлетворяющих заданным условиям
Post::model()->updateCounters($counters,$condition,$params);
~~~

Здесь `$attributes` — это массив значений полей, проиндексированных по имени поля,
`$counters` — массив инкрементных значений, проиндексированных по имени поля,
`$condition` и `$params` аналогичны описанным выше.

Удаление записи
---------------

Мы можем удалить строку, если экземпляр AR был заполнен значениями этой строки.

~~~
[php]
$post=Post::model()->findByPk(10); // предполагаем, что запись с ID=10 существует
$post->delete(); // удаляем строку из таблицы
~~~

Обратите внимание, что после удаления экземпляр AR не меняется, но соответствующей записи в
таблице уже нет.

Следующие методы используются для удаления строк без их предварительной загрузки:

~~~
[php]
// удалим строки, соответствующие указанному условию
Post::model()->deleteAll($condition,$params);
// удалим строки, соответствующие указанному условию и первичному ключу (или нескольким ключам)
Post::model()->deleteByPk($pk,$condition,$params);
~~~

Проверка данных
---------------

Часто во время добавления или обновления строки нам требуется проверить, соответствуют ли
значения полей некоторым правилам. Особенно это важно в случае, если данные поступают со стороны клиента.
В подавляющем большинстве случаев таким данным доверять нельзя.

AR осуществляет проверку данных автоматически в момент вызова метода [save()|CActiveRecord::save].
Проверка основана на правилах, заданных в методе AR-класса [rules()|CModel::rules]. Детально ознакомиться с
тем, как задаются правила проверки, можно в разделе [Определение правил проверки](/doc/guide/form.model#declaring-validation-rules).
Ниже приведён типичный порядок обработки в момент сохранения записи:

~~~
[php]
if($post->save())
{
	// данные корректны и успешно добавлены/обновлены
}
else
{
	// данные некорректны, сообщения об ошибках могут быть получены путём вызова метода getErrors()
}
~~~

В момент, когда данные для добавления или обновления отправляются пользователем через форму ввода, нам требуется
присвоить их соответствующим свойствам AR. Это можно проделать следующим образом:

~~~
[php]
$post->title=$_POST['title'];
$post->content=$_POST['content'];
$post->save();
~~~

Если полей будет много, мы получим простыню из подобных присваиваний. Этого можно избежать, если
использовать свойство [attributes|CActiveRecord::attributes], как показано ниже. Подробности можно найти в
разделах [Безопасное присваивание значений атрибутам](/doc/guide/form.model#securing-attribute-assignments) и
[Создание действия](/doc/guide/form.action).

~~~
[php]
// предполагаем, что $_POST['Post'] является массивом значений полей, проиндексированных по имени поля
$post->attributes=$_POST['Post'];
$post->save();
~~~


Сравнение записей
-----------------

Экземпляры AR идентифицируются уникальным образом по значениям первичного ключа, аналогично строкам таблицы, поэтому
для сравнения двух экземпляров нам нужно просто сравнить значения их первичных ключей, предполагая, что оба экземпляра одного AR-класса.
Однако можно проделать это ещё проще, вызвав метод [CActiveRecord::equals()].

> Info|Информация: В отличие от реализации AR в других фреймворках, Yii поддерживает в AR составные первичные ключи.
Составной первичный ключ состоит из двух и более полей таблицы. Соответственно,
первичный ключ в Yii представлен как массив, а свойство [primaryKey|CActiveRecord::primaryKey]
содержит значение первичного ключа для экземпляра AR.

Тонкая настройка
-------------

Класс [CActiveRecord] предоставляет несколько методов, которые могут быть переопределены в дочерних классах
для тонкой настройки работы AR.

   - [beforeValidate|CModel::beforeValidate] и [afterValidate|CModel::afterValidate]:
методы вызываются до и после осуществления проверки;

   - [beforeSave|CActiveRecord::beforeSave] и [afterSave|CActiveRecord::afterSave]:
методы вызываются до и после сохранения экземпляра AR;

   - [beforeDelete|CActiveRecord::beforeDelete] и [afterDelete|CActiveRecord::afterDelete]:
методы вызываются до и после удаления экземпляра AR;

   - [afterConstruct|CActiveRecord::afterConstruct]: метод вызывается для каждого экземпляра AR,
созданного с использованием оператора `new`;

   - [beforeFind|CActiveRecord::beforeFind]: метод вызывается перед тем, как
     будет выполнен поисковый запрос (например, `find()`, `findAll()`).

   - [afterFind|CActiveRecord::afterFind]: метод вызывается для каждого экземпляра AR,
     созданного в результате выполнения запроса.

Использование транзакций с AR
-----------------------------

Каждый экземпляр AR содержит свойство [dbConnection|CActiveRecord::dbConnection], которое является экземпляром класса [CDbConnection].
Поэтому при необходимости можно использовать [транзакции](/doc/guide/database.dao#using-transactions),
предоставляемые Yii DAO:

~~~
[php]
$model=Post::model();
$transaction=$model->dbConnection->beginTransaction();
try
{
	// поиск и сохранение — шаги, между которыми могут быть выполнены другие запросы,
	// поэтому мы используем транзакцию, чтобы удостовериться в целостности данных
	$post=$model->findByPk(10);
	$post->title='new post title';
	if($post->save())
		$transaction->commit();
	else
		$transaction->rollback();
}
catch(Exception $e)
{
	$transaction->rollback();
	throw $e;
}
~~~

Именованные группы условий
--------------------------

> Info|Информация: Идея групп условий позаимствована у Ruby on Rails.

*Именованная группа условий* представляет собой *именованный* критерий запроса,
который можно использовать с другими группами и применять к запросам AR.

Именованные группы чаще всего описываются в методе [CActiveRecord::scopes()]
в виде пар имя-условие. Приведённый ниже код описывает две именованные группы
условий для модели `Post`: `published` и `recently`:

~~~
[php]
class Post extends CActiveRecord
{
	…
	public function scopes()
	{
		return array(
			'published'=>array(
				'condition'=>'status=1',
			),
			'recently'=>array(
				'order'=>'create_time DESC',
				'limit'=>5,
			),
		);
	}
}
~~~

Каждая группа описывается массивом, который используется для инициализации
экземпляра [CDbCriteria]. К примеру, `recently` устанавливает значение `order`
равным `create_time DESC`, а `limit` равным 5. Вместе эти условия означают, что
будут выбраны пять последних публикаций.

Именованные группы условий обычно используются в качестве модификаторов для
метода `find`. Можно использовать несколько групп одновременно для получения более точного
результата. Например, чтобы найти последние опубликованные записи, можно использовать
следующий код:

~~~
[php]
$posts=Post::model()->published()->recently()->findAll();
~~~

Именованные группы условий следует располагать в цепочке левее метода `find`.
Каждая группа определяет критерий запроса, который объединяется с остальными
критериями, включая переданные непосредственно методу `find`.
Конечный результат можно рассматривать как применение к запросу нескольких фильтров.

> Note|Примечание: Именованные группы могут быть использованы только совместно с методами уровня
класса. Таким образом, метод должен вызываться при помощи `ClassName::model()`.


### Именованные группы условий с параметрами

Именованные группы условий могут быть параметризованы.
Например, если нам необходимо изменять число выбираемых публикаций для группы `recently`, то
вместо описания группы в методе [CActiveRecord::scopes], мы должны создать новый метод с именем, совпадающим с названием группы условий:

~~~
[php]
public function recently($limit=5)
{
	$this->getDbCriteria()->mergeWith(array(
		'order'=>'create_time DESC',
		'limit'=>$limit,
	));
	return $this;
}
~~~

Теперь, чтобы получить 3 последних опубликованных записи, можно
использовать следующий код:

~~~
[php]
$posts=Post::model()->published()->recently(3)->findAll();
~~~

Если явно не указать значение аргумента, то по умолчанию будут выбраны 5 последних опубликованных записей.


### Группа условий по умолчанию

Класс модели может содержать группу условий по умолчанию, которая будет
применяться ко всем запросам (включая реляционные). К примеру, на сайте реализована
поддержка нескольких языков, и содержимое отображается на языке, выбранном пользователем.
Так как запросов, связанных с получением данных, скорее всего, будет достаточно много,
для решения этой задачи мы можем определить группу условий по умолчанию.
Для этого необходимо переопределить метод [CActiveRecord::defaultScope] следующим образом:

~~~
[php]
class Content extends CActiveRecord
{
	public function defaultScope()
	{
		return array(
			'condition'=>"language='".Yii::app()->language."'",
		);
	}
}
~~~

Теперь к приведённому ниже вызову метода `findAll` будут автоматически применены наши условия:

~~~
[php]
$contents=Content::model()->findAll();
~~~

> Note|Примечание: Как группа условий по умолчанию, так и именованная группа условий
> применяются только к запросам типа `SELECT` и игнорируются при запросах вида
> `INSERT`, `UPDATE` или `DELETE`. Кроме того, нельзя использовать AR-модель для выполнения запросов
> внутри методов, объявляющих группы условий (как именованные, так и группы условий по умолчанию).